• ABOUT
  • 2017
  • 2018
  • 2019
  • 2020
  • 2021
  • COMPARAISON

Load all necessary libraries¶

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import squarify
import folium
import json
import seaborn as sbn
from urllib.request import urlopen
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')

Import data¶

In [2]:
fullData = pd.read_csv('valeursfoncieres-2017.txt', sep='|')

fullData2017 = pd.read_csv('valeursfoncieres-2017.txt', sep='|')
fullData2018 = pd.read_csv('valeursfoncieres-2018.txt', sep='|')
fullData2019 = pd.read_csv('valeursfoncieres-2019.txt', sep='|')
fullData2020 = pd.read_csv('valeursfoncieres-2020.txt', sep='|')
fullData2021 = pd.read_csv('valeursfoncieres-2021.txt', sep='|')
In [3]:
fullData
Out[3]:
Code service CH Reference document 1 Articles CGI 2 Articles CGI 3 Articles CGI 4 Articles CGI 5 Articles CGI No disposition Date mutation Nature mutation ... Surface Carrez du 5eme lot Nombre de lots Code type local Type local Identifiant local Surface reelle bati Nombre pieces principales Nature culture Nature culture speciale Surface terrain
0 NaN NaN NaN NaN NaN NaN NaN 1 02/01/2017 Vente ... NaN 2 2.0 Appartement NaN 37.0 2.0 NaN NaN NaN
1 NaN NaN NaN NaN NaN NaN NaN 1 05/01/2017 Vente ... NaN 0 NaN NaN NaN NaN NaN AB NaN 788.0
2 NaN NaN NaN NaN NaN NaN NaN 1 06/01/2017 Vente ... NaN 0 NaN NaN NaN NaN NaN P NaN 42.0
3 NaN NaN NaN NaN NaN NaN NaN 1 06/01/2017 Vente ... NaN 0 NaN NaN NaN NaN NaN P NaN 77.0
4 NaN NaN NaN NaN NaN NaN NaN 1 06/01/2017 Vente ... NaN 0 NaN NaN NaN NaN NaN P NaN 94.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3383617 NaN NaN NaN NaN NaN NaN NaN 1 20/12/2017 Vente ... NaN 3 4.0 Local industriel. commercial ou assimilé NaN 90.0 0.0 NaN NaN NaN
3383618 NaN NaN NaN NaN NaN NaN NaN 1 16/10/2017 Vente ... NaN 1 2.0 Appartement NaN 19.0 1.0 NaN NaN NaN
3383619 NaN NaN NaN NaN NaN NaN NaN 1 16/10/2017 Vente ... NaN 1 4.0 Local industriel. commercial ou assimilé NaN 55.0 0.0 NaN NaN NaN
3383620 NaN NaN NaN NaN NaN NaN NaN 1 08/12/2017 Vente ... NaN 1 2.0 Appartement NaN 26.0 2.0 NaN NaN NaN
3383621 NaN NaN NaN NaN NaN NaN NaN 1 06/12/2017 Vente ... NaN 2 4.0 Local industriel. commercial ou assimilé NaN 62.0 0.0 NaN NaN NaN

3383622 rows × 43 columns

Clean data¶

In [4]:
columns_to_keep = ['Date mutation','Nature mutation','Valeur fonciere','Code postal','Commune','Code departement','Code commune','Nombre de lots','Code type local','Type local','Surface reelle bati','Nombre pieces principales','Surface terrain']
fullData['Date mutation'] = pd.to_datetime(fullData['Date mutation'])
fullData['Code departement'] = fullData['Code departement'].astype(str)
fullData = fullData[columns_to_keep]
fullData = fullData.dropna()
fullData['Valeur fonciere'] = pd.to_numeric(fullData['Valeur fonciere'].str.replace(',', '.'))
fullData
Out[4]:
Date mutation Nature mutation Valeur fonciere Code postal Commune Code departement Code commune Nombre de lots Code type local Type local Surface reelle bati Nombre pieces principales Surface terrain
6 2017-03-01 Vente 258000.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 2.0 Appartement 22.0 1.0 655.0
7 2017-03-01 Vente 258000.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 2.0 Appartement 22.0 1.0 655.0
8 2017-03-01 Vente 258000.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 3.0 Dépendance 0.0 0.0 655.0
9 2017-03-01 Vente 258000.0 1000.0 SAINT-DENIS-LES-BOURG 1 344 0 2.0 Appartement 120.0 5.0 655.0
10 2017-05-01 Vente 175050.0 1370.0 VAL-REVERMONT 1 426 0 1.0 Maison 99.0 5.0 471.0
... ... ... ... ... ... ... ... ... ... ... ... ... ...
3383569 2017-12-15 Vente 11328000.0 75002.0 PARIS 02 75 102 0 2.0 Appartement 75.0 3.0 704.0
3383570 2017-12-15 Vente 11328000.0 75002.0 PARIS 02 75 102 0 4.0 Local industriel. commercial ou assimilé 178.0 0.0 704.0
3383571 2017-12-15 Vente 11328000.0 75002.0 PARIS 02 75 102 0 4.0 Local industriel. commercial ou assimilé 207.0 0.0 704.0
3383572 2017-12-15 Vente 11328000.0 75002.0 PARIS 02 75 102 0 2.0 Appartement 65.0 3.0 704.0
3383573 2017-12-15 Vente 11328000.0 75002.0 PARIS 02 75 102 0 2.0 Appartement 65.0 3.0 704.0

914098 rows × 13 columns

Argent total dépensé par mois selon les types de mutation pendant l'année 2017¶

In [5]:
MUTATIONS = fullData['Nature mutation'].unique()
def plotMutations(mut, data, ax):

    for m in MUTATIONS:
        temp = data[data['Nature mutation'] == m]
        result = temp.groupby(temp['Date mutation'].dt.to_period("M"))['Valeur fonciere'].sum()
        result.index = result.index.to_timestamp()
        x = result.index
        y = result.values
        
        if m == mut:
            ax.plot(x, y, color="#0b53c1", lw=2.4, zorder=10)
            ax.scatter(x, y, fc="w", ec="#0b53c1", s=60, lw=2.4, zorder=12)  
            ax.autoscale()    
        else:
            ax.plot(x, y, color="#BFBFBF", lw=1.5)
    
    ax.set_title(mut, fontfamily="Inconsolata", fontsize=14, fontweight=500)
    return ax
In [6]:
fig, axes = plt.subplots(2, 3, figsize=(14, 7.5))
for idx, (ax, mut) in enumerate(zip(axes.ravel(), MUTATIONS)):
    # Only annotate the first panel
    annotate = idx == 0
    plotMutations(mut, fullData, ax)
findfont: Font family ['Inconsolata'] not found. Falling back to DejaVu Sans.

On remarque que la plupart des mutations au cours de l'année sont des ventes, mis à part un pic d'expropriation

In [30]:
data1 = fullData[fullData['Nature mutation'] =='Vente'] 
data1 = data1.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data1.index = data1.index.to_timestamp()

data2 = fullData[fullData['Nature mutation'] =='Vente terrain à bâtir'] 
data2 = data2.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data2.index = data2.index.to_timestamp()

data3 = fullData[fullData['Nature mutation'] =='Echange'] 
data3 = data3.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data3.index = data3.index.to_timestamp()

data4 = fullData[fullData['Nature mutation'] =="Vente en l'état futur d'achèvement"] 
data4 = data4.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data4.index = data4.index.to_timestamp()

data5 = fullData[fullData['Nature mutation'] =='Adjudication'] 
data5 = data5.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data5.index = data5.index.to_timestamp()

data6 = fullData[fullData['Nature mutation'] =='Expropriation'] 
data6 = data6.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data6.index = data6.index.to_timestamp()

plt.figure(figsize=(18,10))
plt.plot(data1.index, data1.values, "r--", color="red")
plt.plot(data2.index, data2.values, "r--", color="blue")
plt.plot(data3.index, data3.values, "r--", color="green")
plt.plot(data4.index, data4.values, "r--", color="yellow")
plt.plot(data5.index, data5.values, "r--", color="purple")
plt.plot(data6.index, data6.values, "r--", color="black")
plt.legend(['Vente','Vente terrain à bâtir', 'Echange',"Vente en l'état futur d'achèvement",'Adjudication','Expropriation'])
plt.title('Nombre de mutations par type au cours des mois, en cumulé')
plt.show()

On peut encore une fois confirmer que le seul type de mutation importante est la vente.

Nombre et répartitions des types de locaux¶

In [35]:
data = fullData.groupby(['Type local'])['Type local'].count()


plt.bar(data.index, data.values)
bars = ['Appartement', 'Dépendance', 'Industriel', 'Maison']
y_pos = np.arange(len(bars))
plt.xticks(y_pos, bars)
plt.title('Nombre de mutations par type de local')
Out[35]:
Text(0.5, 1.0, 'Nombre de mutations par type de local')
In [34]:
perc = [f'{i/data.values.sum()*100:5.2f}%' for i in data.values]
lbl = [f'{j[0]} = {j[1]}' for j in zip(data.index, perc)]

squarify.plot(sizes=data.values, label=lbl)
plt.axis('off')
plt.title('Proportion des types de locaux sur le nombre total de mutations')
plt.show()

On remarque que les mutations concernent principalement des maisons et des appartements.

In [36]:
data=fullData[fullData["Surface terrain"]< 5000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface terrain", data=data)
plt.title('Répartition des types de locaux selon la surface de leur terrain')
Out[36]:
Text(0.5, 1.0, 'Répartition des types de locaux selon la surface de leur terrain')
In [37]:
data=fullData[(fullData["Surface reelle bati"]< 1000) & (fullData["Surface reelle bati"].notna())]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface reelle bati", data=data)
plt.title('Répartition des types de locaux selon leur surface réelle bâtie')
Out[37]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur surface réelle bâtie')

On voit ici que les données des dépendances sont assez peu intéressantes, puisque leur surface réelle bâtie est proche de zéro.

In [58]:
data = fullData[fullData['Valeur fonciere'] < 2000000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x="Type local",y="Valeur fonciere",data=data)
plt.title('Répartition des types de locaux selon leur valeur foncière')
Out[58]:
Text(0.5, 1.0, 'Répartition des types de locaux selon leur valeur foncière')

Analyse des données par département¶

In [38]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count().sort_values(ascending=True)
plt.figure(figsize=(10,20))

plt.hlines(y=data.index, xmin=0, xmax=data.values, color='purple')
plt.plot(data.values, data.index, "o", color="gold")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title('Nombre de mutations par département')
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()
In [40]:
data1 = fullData.groupby(['Code departement'])['Nature mutation'].count()

data = fullData[((fullData['Nature mutation']=='Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement')))]
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
plt.figure(figsize=(10,20))

plt.hlines(y=data1.index, xmin = 0, xmax = data1.values, color='red')
plt.hlines(y=data.index, xmin=0, xmax=data.values, color='skyblue')
plt.plot(data.values, data.index, "o")
plt.plot(data1.values, data1.index, "x", color="white")
 
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title("Nombre de mutations par département (ronds) et nombre de mutations par département en considérant uniquement les ventes d'appartements et de maisons (croix)")
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()

On ne fait ici que confirmer visuellement que les ventes d'appartement et de maisons constituent la majorité écrasante de mutations, avec une variation extrêmement faible pour certains départements.

In [44]:
myscale = None

def mapping_france_folium(data):
    map = folium.Map(location=[48.862, 2.346], zoom_start = 5)
    departments = f"https://france-geojson.gregoiredavid.fr/repo/departements.geojson"
    d = {'Code': data.index, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)

    folium.Choropleth(geo_data=departments, 
    data=da, 
    columns=['Code', 'Valeur'], 
    key_on='properties.code',
    fill_color= "PuRd",
    fill_opacity=1,
    line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map


def mapping_Paris_circle(data, bigNumbers = False):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index, 'Valeur': data.values}
    da = pd.DataFrame(d)
    for a in arr["features"]:
        prop = a["properties"]
        temp = da[da['Code'] == prop["c_arinsee"] - 100]
        temp = temp['Valeur'].values
        folium.Circle(prop["geom_x_y"], 
        fill=True,
        popup = prop["l_ar"],
        radius = (temp[0]/1) if not bigNumbers else temp[0]/9000000).add_to(map)
    return map


def mapping_Paris(data):
    map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
    arr = json.load(open("arrondissements.geojson"))
    d = {'Code': data.index + 100, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 75100) & (da['Code'] <= 75120)]
    myscale = np.linspace(da['Valeur'].min(), da['Valeur'].max(), 10)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.c_arinsee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
        
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Lyon(data):
    map = folium.Map(location = [45.763420, 4.834277], zoom_start = 12)
    arr = json.load(open("adr_voie_lieu.json"))
    d = {'Code': data.index + 380, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 69381) & (da['Code'] <= 69389)]
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.insee',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map

def mapping_Marseille(data):
    map = folium.Map(location = [43.296482, 5.36978], zoom_start = 12)
    arr = json.load(open("quartiers-marseille.geojson"))
    d = {'Code': data.index + 200, 'Valeur': np.log(data.values)}
    da = pd.DataFrame(d)
    da = da[(da['Code'] >= 13201) & (da['Code'] <= 13216)]
    da['Code'] = da['Code'].astype(int).astype(str)
    folium.Choropleth(geo_data=arr, 
            data=da, 
            columns=['Code', 'Valeur'], 
            key_on='properties.DEPCO',
            fill_color= "PuRd",
            threshold_scale=myscale,
            fill_opacity=0.8,
            line_opacity=.1).add_to(map)
    
    folium.LayerControl().add_to(map)
    return map
In [15]:
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
map = mapping_france_folium(data)
map
Out[15]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ici, en échelle logarithmique, le nombre de mutations par département au cours de l'année.

In [16]:
data = fullData[fullData['Nature mutation'] == 'Vente'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[16]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes par département au cours de l'année.

In [17]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data[data['Type local'] == 'Maison'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
Out[17]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes de maisons par département au cours de l'année. Il est intéressant de noter que l'importance de Paris dans la carte précédente disparaît : très peu de maisons sont vendues à Paris même.

In [18]:
data = fullData[(fullData['Nature mutation'] == 'Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement'))]
data['prix_m2'] = data['Valeur fonciere']/data['Surface reelle bati']
data = data.groupby(['Code departement'])['prix_m2'].mean()
#data = data.to_frame()
map = mapping_france_folium(data)
map
Out[18]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus, en échelle logarithmique, le prix au m2 par département. On remarque une certaine corrélation entre le nombre de ventes par département et le prix au m2, qu'on cherchera à confirmer par la suite

In [55]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie')
plt.show()

On cherche ici à montrer l'existence de valeurs extrêmes dans les données, qui nous ont forcé à adopter une échelle logarithmique pour les cartes, sans quoi nous aurions dû filtrer ces valeurs extrême (ci-dessous).

In [57]:
data = fullData[['Surface reelle bati','Valeur fonciere']]
data = data[data['Valeur fonciere'] < 1000000]
data = data[data['Surface reelle bati'] < 2000]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie, sans les valeurs extrêmes')
plt.show()

Analyse plus localisée sur Paris¶

Nombre de mutations par arrondissement¶

In [20]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris_circle(data, False)
map
Out[20]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus le nombre de mutations par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

In [45]:
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris(data)
map
Out[45]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Valeur des ventes par arrondissement¶

In [46]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris_circle(data, True)
map
Out[46]:
Make this Notebook Trusted to load map: File -> Trust Notebook

On voit ci-dessus la valeur des ventes par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.

On va s'intéresser par la suite à la valeur des ventes par arrondissement entre Paris, Lyon et Marseille. On pourra observer que la localisation des quartiers les plus recherchés est bien différente entre ces grandes villes.

In [47]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris(data)
map
Out[47]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [48]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Marseille(data)
map
Out[48]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [49]:
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Lyon(data)
map
Out[49]:
Make this Notebook Trusted to load map: File -> Trust Notebook

¶

In [53]:
data = fullData[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]

sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en France))')
plt.show()
In [54]:
data = fullData[fullData['Code departement'] == '75']
data = data[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]
sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en IDF))')
plt.show()

On remarquera une grande différence entre les données de corrélation en France, et en Île de France. Les données semblent beaucoup plus liées en Île de France, notamment la surface du terrain, la surface réele bâtie et la valeur foncière.